Skip to content

Method: static {...}

1: /*
2: * JOPA
3: * Copyright (C) 2024 Czech Technical University in Prague
4: *
5: * This library is free software; you can redistribute it and/or
6: * modify it under the terms of the GNU Lesser General Public
7: * License as published by the Free Software Foundation; either
8: * version 3.0 of the License, or (at your option) any later version.
9: *
10: * This library is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13: * Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public
16: * License along with this library.
17: */
18: package cz.cvut.kbss.jopa.loaders;
19:
20: import cz.cvut.kbss.jopa.exceptions.OWLPersistenceException;
21: import org.slf4j.Logger;
22: import org.slf4j.LoggerFactory;
23:
24: import java.io.File;
25: import java.io.IOException;
26: import java.net.URI;
27: import java.net.URL;
28: import java.net.URLDecoder;
29: import java.nio.charset.StandardCharsets;
30: import java.util.ArrayList;
31: import java.util.Enumeration;
32: import java.util.HashSet;
33: import java.util.List;
34: import java.util.Set;
35: import java.util.function.Consumer;
36: import java.util.jar.JarEntry;
37: import java.util.jar.JarFile;
38:
39: /**
40: * Processes classes available to the current classloader.
41: */
42: public class DefaultClasspathScanner implements ClasspathScanner {
43:
44: private static final Logger LOG = LoggerFactory.getLogger(DefaultClasspathScanner.class);
45:
46: protected static final char JAVA_CLASSPATH_SEPARATOR = '/';
47: protected static final char WINDOWS_FILE_SEPARATOR = '\\';
48: protected static final char JAVA_PACKAGE_SEPARATOR = '.';
49: protected static final String JAR_FILE_SUFFIX = ".jar";
50: protected static final String CLASS_FILE_SUFFIX = ".class";
51:
52: protected final List<Consumer<Class<?>>> listeners = new ArrayList<>();
53:
54: protected final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
55:
56: protected String pathPattern;
57: protected Set<String> visited;
58:
59: @Override
60: public void addListener(Consumer<Class<?>> listener) {
61: listeners.add(listener);
62: }
63:
64: /**
65: * Inspired by <a
66: * href="https://github.com/ddopson/java-class-enumerator">https://github.com/ddopson/java-class-enumerator</a>
67: */
68: @Override
69: public void processClasses(String scanPackage) {
70: this.pathPattern = scanPackage.replace(JAVA_PACKAGE_SEPARATOR, JAVA_CLASSPATH_SEPARATOR);
71: this.visited = new HashSet<>();
72: try {
73: Enumeration<URL> urls = classLoader.getResources(pathPattern);
74: processElements(urls, scanPackage);
75: // Scan jar files on classpath
76: Enumeration<URL> resources = classLoader.getResources(".");
77: processElements(resources, scanPackage);
78: } catch (IOException e) {
79: throw new OWLPersistenceException("Unable to scan packages for entity classes.", e);
80: }
81: }
82:
83: protected void processElements(Enumeration<URL> urls, String scanPath) throws IOException {
84: while (urls.hasMoreElements()) {
85: final URL url = urls.nextElement();
86: final String elemUri = url.toString();
87: if (visited.contains(elemUri)) {
88: continue;
89: }
90: visited.add(elemUri);
91: LOG.trace("Processing classpath element {}", elemUri);
92: if (isJar(elemUri)) {
93: final JarFile jarFile = createJarFile(url);
94: if (!elemUri.equals(jarFile.getName()) && visited.contains(jarFile.getName())) {
95: LOG.trace("Classpath element {} maps to a JAR file {} and it has already been processed.", elemUri, jarFile.getName());
96: } else {
97: visited.add(jarFile.getName());
98: processJarFile(jarFile);
99: }
100: } else {
101: processDirectory(new File(URI.create(elemUri).getPath()), scanPath);
102: }
103: }
104: }
105:
106: /**
107: * Handles possible non-ascii character encoding in the specified URL.
108: *
109: * @param url Resource URL (presumably leading to a local file)
110: * @return Decoded argument
111: */
112: protected static String sanitizePath(URL url) {
113: return URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
114: }
115:
116: protected static boolean isJar(String filePath) {
117: return filePath.startsWith("jar:") || filePath.endsWith(JAR_FILE_SUFFIX);
118: }
119:
120: protected static JarFile createJarFile(URL elementUrl) throws IOException {
121: final String jarPath = sanitizePath(elementUrl).replaceFirst("[.]jar/?!.*", JAR_FILE_SUFFIX)
122: .replaceFirst("file:", "")
123: .replaceFirst("nested:", "");
124: return new JarFile(jarPath);
125: }
126:
127: /**
128: * Processes the specified {@link JarFile}, looking for classes in the configured package.
129: *
130: * @param jarFile JAR file to scan
131: */
132: protected void processJarFile(final JarFile jarFile) {
133: LOG.trace("Scanning jar file {} for entity classes.", jarFile.getName());
134: try (final JarFile localFile = jarFile) {
135: final Enumeration<JarEntry> entries = localFile.entries();
136: while (entries.hasMoreElements()) {
137: final JarEntry entry = entries.nextElement();
138: final String entryName = entry.getName();
139: if (entryName.endsWith(CLASS_FILE_SUFFIX) && entryName.contains(pathPattern)) {
140: String className = entryName.substring(entryName.indexOf(pathPattern));
141: className = className.replace(JAVA_CLASSPATH_SEPARATOR, JAVA_PACKAGE_SEPARATOR)
142: .replace(WINDOWS_FILE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
143: className = className.substring(0, className.length() - CLASS_FILE_SUFFIX.length());
144: processClass(className);
145: }
146: }
147: } catch (IOException e) {
148: throw new OWLPersistenceException("Unexpected IOException reading JAR File " + jarFile, e);
149: }
150: }
151:
152: /**
153: * Retrieves a {@link Class} with the specified name and passes it to the registered listeners.
154: *
155: * @param className Fully-qualified class name
156: */
157: protected void processClass(String className) {
158: try {
159: final Class<?> cls = Class.forName(className, true, classLoader);
160: listeners.forEach(listener -> listener.accept(cls));
161: } catch (Exception | NoClassDefFoundError e) {
162: LOG.debug("Unable to load class {}, got error {}: {}. Skipping the class. If it is an entity class, ensure it is available on classpath and is built with supported Java version.", className, e.getClass()
163: .getName(), e.getMessage());
164: }
165: }
166:
167: /**
168: * Processes the specified directory, looking for classes in the specified package (and its descendants).
169: *
170: * @param dir Directory
171: * @param packageName Package name
172: */
173: protected void processDirectory(File dir, String packageName) throws IOException {
174: if (!dir.getPath().replace(WINDOWS_FILE_SEPARATOR, JAVA_CLASSPATH_SEPARATOR).contains(pathPattern)) {
175: return;
176: }
177: LOG.trace("Scanning directory {} for entity classes.", dir);
178: // Get the list of the files contained in the package
179: final String[] files = dir.list();
180: if (files == null) {
181: return;
182: }
183: for (String fileName : files) {
184: String className = null;
185: // we are only interested in .class files
186: if (fileName.endsWith(CLASS_FILE_SUFFIX)) {
187: // removes the .class extension
188: className = packageName + '.' + fileName.substring(0, fileName.length() - 6);
189: }
190: if (className != null) {
191: processClass(className);
192: }
193: final File subDir = new File(dir, fileName);
194: if (subDir.isDirectory()) {
195: processDirectory(subDir, packageName + (!packageName.isEmpty() ? JAVA_PACKAGE_SEPARATOR : "") + fileName);
196: } else if (isJar(subDir.getAbsolutePath())) {
197: processJarFile(createJarFile(subDir.toURI().toURL()));
198: }
199: }
200: }
201: }